/**
 * 
 */
package persistence;

import java.util.HashSet;
import java.util.Set;

import location.ECEFCoordinates;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;

import org.apache.log4j.Logger;
import org.openrdf.model.URI;
import org.openrdf.query.MalformedQueryException;
import org.openrdf.query.QueryEvaluationException;
import org.openrdf.repository.RepositoryException;
import org.openrdf.repository.object.ObjectConnection;
import org.openrdf.repository.object.ObjectQuery;
import org.openrdf.repository.object.annotations.iri;
import org.openrdf.result.Result;

import activity.Activity;

/**
 * Persistent activity entity. An Activity is something one wants to do, e.g.
 * visiting the Golden Roof. IMPORTANT: In order to properly remove an entity
 * from the repository the clearFields methods has to set all fields to null.
 * IMPORTANT NOTE: Activites that are created by the default constructor are NOT
 * persistant in any way.
 * 
 * @author Stefan
 * @see AlibabaObjectRepository
 * @see #removeInstance(String)
 * @see #clearFields(ActivityEntity)
 */
@iri(ActivityEntity.NS + "Activity")
public class ActivityEntity implements Activity {
	private static Logger logger = Logger.getLogger(ActivityEntity.class);
	public static final String NS = "http://activityserver.sti.at/activity#";

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasId")
	private String id; // don't use name => alibaba bug

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasTitle")
	private String title;

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasDescription")
	private String description;

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasX")
	private Double x; // x coordinate of ecef meeting point

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasY")
	private Double y; // y coordinate of ecef meeting point

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasZ")
	private Double z; // z coordinate of ecef meeting point

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasLatitude")
	private Double latitude; // latitude meeting point

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasLongitude")
	private Double longitude; // longitude meeting point

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasAltitude")
	private Double altitude; // altitude meeting point

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasStartTime")
	private Long startTime; // time the activity is going to take place

	@Getter(AccessLevel.PUBLIC)
	@Setter(AccessLevel.PUBLIC)
	@iri(NS + "hasEndTime")
	private Long endTime; // time the activity is going to take place

	/**
	 * Find an Activity by its id.
	 * 
	 * @param id
	 * @return the Activity if found, null otherwise
	 */
	public static Activity getInstance(String activityName) {
		ActivityEntity activity = null;
		URI id = getURIfrom(activityName);
		ObjectConnection connection = AlibabaObjectRepository.getConnection();

		try {

			activity = connection.getObject(ActivityEntity.class, id);
			logger.trace("getInstance of " + id + " name "
					+ activity.getDescription());

		} catch (ClassCastException ce) { // element not found
			logger.trace("instance " + id + "not found.");
			return null;
		} catch (RepositoryException e) {
			logger.error(" failed to get instance  " + id + " due to "
					+ e.getMessage());
		} catch (QueryEvaluationException e) {
			logger.error(" failed to get instance  " + id + " due to "
					+ e.getMessage());
		}

		return activity;
	}

	public static Activity getInstance(String activityName, boolean loadFields) {
		Activity activity = getInstance(activityName);
		if (loadFields) {
			if (null != activity) {
				ActivityEntity.updateFields(activity);
			}
		}
		return activity;
	}

	public static URI getURIfrom(String activityId) {
		return AlibabaObjectRepository.getConnection().getValueFactory()
				.createURI(NS + activityId);
	}

	/**
	 * Creates a new, persistent Activity.
	 * 
	 * @param id
	 * @return
	 */
	public static Activity newInstance(String activityId) {
		ActivityEntity newActivity = new ActivityEntity();
		newActivity.setId(activityId);
		URI idUri = getURIfrom(activityId);
		try {
			logger.trace("adding new Instance " + activityId);
			AlibabaObjectRepository.getConnection().addObject(idUri,
					newActivity);
			AlibabaObjectRepository.getConnection().commit();
			return getInstance(activityId, true);
		} catch (RepositoryException e) {
			logger.error(" failed to create new " + activityId + " due to "
					+ e.getMessage());
			return null;
		}

	}

	/**
	 * Removes an activity from the RDF triple store.
	 * 
	 * @param activityId
	 */
	public static void removeInstance(String activityId) {
		Activity activity = null;
		URI id = getURIfrom(activityId);
		ObjectConnection connection = AlibabaObjectRepository.getConnection();

		try {
			activity = getInstance(activityId);

			// set all fields null
			clearFields(activity);
			connection.removeDesignation(activity, ActivityEntity.class);

			connection.commit();
		} catch (ClassCastException ce) { // element not found
			return;
		} catch (RepositoryException e) {
			logger.error(" failed to get instance  " + id + " due to "
					+ e.getMessage());
		}
	}

	/**
	 * Returns all Activities that are in location range and that have not
	 * already taken place.<br/>
	 * Documentation:
	 * <ul>
	 * <li>SERQL: {@link http://www.openrdf.org/doc/sesame/users/ch06.html}</li>
	 * <li>SPARQL: {@link http://www.w3.org/TR/rdf-sparql-query/#func-datatype}</li>
	 * <li>ObjectRepository: {@link http
	 * ://www.w3.org/TR/rdf-sparql-query/#func-datatype}</li>
	 * </ul>
	 * 
	 * @param range
	 *            the range in metres an Activity has to be
	 * @return
	 */
	public static Set<Activity> getInstances(Double latitude, Double longitude,
			Double altitude, Integer range, Long startTime, Long endTime,
			Set<String> categories, String activityTitle,
			String activityDescription) {
		Set<Activity> activitiesInRange = new HashSet<Activity>();
		// check what kind of parameters are set
		boolean rangeNotSpecified = (null == latitude) || (null == longitude)
				|| (null == altitude) || (null == range);
		boolean rangeSpecified = !rangeNotSpecified;

		if (rangeNotSpecified) {
			logger
					.info(" range and position not properly specified. (lat/lon/alt/ran) ("
							+ latitude
							+ "/"
							+ longitude
							+ "/"
							+ altitude
							+ "/"
							+ range + ")");
			return activitiesInRange;
		}
		ECEFCoordinates queryLocation = new ECEFCoordinates(latitude,
				longitude, altitude);

		ObjectConnection connection = AlibabaObjectRepository.getConnection();
		// define dynamic query
		try {

			String getActivitiesInRangeQueryString = "PREFIX as:<"
					+ ActivityEntity.NS + ">\n"
					+ "SELECT ?activities  \n"
					+ "WHERE { \n"
					// location query
					+ "?activities as:hasX ?x." + "?activities as:hasY ?y."
					+ "?activities as:hasZ ?z."
					// time query
					+ "?activities as:hasStartTime ?starttime."
					//+ "?activities as:hasEndTime ?endtime."
					// title query
					+ "?activities as:hasTitle ?title."

					// define query filter
					+ "FILTER (";
			// add time of event must be greater than current time

			getActivitiesInRangeQueryString += " (?starttime >= ?currentTime) ";

			if (null != startTime) {
				getActivitiesInRangeQueryString += " && (?starttime <= ?startTime) ";
			}
			//if (null != endTime) {
			//	getActivitiesInRangeQueryString += " && (?endtime <= ?endTime) ";
			//}

			// within range part

			getActivitiesInRangeQueryString += "&& ( "
					+ "(?x < (?currentX + ?halfRange))" + " && "
					+ "(?x > (?currentX - ?halfRange))" + " && "

					+ "(?y < (?currentY + ?halfRange))" + " && "
					+ "(?y > (?currentY - ?halfRange))" + " && "

					+ "(?z < (?currentZ + ?halfRange))" + " && "
					+ "(?z > (?currentZ - ?halfRange)))" ;
					
			getActivitiesInRangeQueryString	+= ") } "; // end query
																		// end

			ObjectQuery activitiesInRangeQuery = connection
					.prepareObjectQuery(getActivitiesInRangeQueryString);
			activitiesInRangeQuery.setObject("currentTime", System
					.currentTimeMillis());
			activitiesInRangeQuery.setObject("currentX", queryLocation.getX());
			activitiesInRangeQuery.setObject("currentY", queryLocation.getY());
			activitiesInRangeQuery.setObject("currentZ", queryLocation.getZ());
			activitiesInRangeQuery.setObject("halfRange", range / 2);
			// + "");

			Result<ActivityEntity> queryResult = activitiesInRangeQuery
					.evaluate(ActivityEntity.class);
			activitiesInRange = new HashSet<Activity>(queryResult.asList());
			logger.trace("executed: " + getActivitiesInRangeQueryString +" with "+activitiesInRange.size()+" results.");

		} catch (MalformedQueryException e) {
			logger
					.error(" invalid getActivitiesInRange query. check query for syntax errors. "
							+ e.getMessage());
		} catch (RepositoryException e) {
			logger
					.error(" could not create/execute activitesInRangeQuery due to: "
							+ e.getMessage());
		}
		// execute query
		catch (QueryEvaluationException e) {
			logger.error(" could not execute activitesInRangeQuery due to: "
					+ e.getMessage());
		}

		// parse results

		return activitiesInRange;
	}
	/**
	 * copies all fields to a new instance of ActivityEntity. This is needed to create a copy for JSON export, because ActivityEntities-proxies cannot be exported.
	 * @see updateFields
	 * @return
	 */
	public Activity getInstanceCopy() {
		ActivityEntity activityCopy = new ActivityEntity();
		activityCopy.setId(this.getId());
		activityCopy.setTitle(this.getTitle());
		activityCopy.setDescription(this.getDescription());
		activityCopy.setX(this.getX());
		activityCopy.setY(this.getY());
		activityCopy.setZ(this.getZ());
		activityCopy.setEndTime(this.getEndTime());
		activityCopy.setStartTime(this.getStartTime());
		activityCopy.setAltitude(this.getAltitude());
		activityCopy.setLatitude(this.getLatitude());
		activityCopy.setLongitude(this.getLongitude());
		return activityCopy;
	}

	/**
	 * Sets all fields of an {@code Activity} to null. Needed to remove the
	 * instance from the repository.
	 * 
	 * @param activity
	 * @see Activity#removeInstance(String)
	 */
	private static void clearFields(Activity activity) {
		activity.setId(null);
		activity.setTitle(null);
		activity.setDescription(null);
		activity.setX(null);
		activity.setY(null);
		activity.setZ(null);
		activity.setEndTime(null);
		activity.setStartTime(null);
		activity.setAltitude(null);
		activity.setLatitude(null);
		activity.setLongitude(null);
	}

	/**
	 * Loads all fields from the Alibaba - proxy.
	 * 
	 * @param activity
	 * @see Activity#removeInstance(String)
	 */
	private static void updateFields(Activity activity) {
		activity.getId();
		activity.getTitle();
		activity.getDescription();
		activity.getX();
		activity.getY();
		activity.getZ();
		activity.getEndTime();
		activity.getStartTime();
		activity.getAltitude();
		activity.getLatitude();
		activity.getLongitude();
	}

}
